package com.shoufuyou.sdk;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.Map.Entry;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;

public class SfyClient {
    private SfyConfig config;

    public SfyClient(SfyConfig config) {
        this.config = config;
    }

    public TradeQueryResponse sendTradeQueryRequest(TradeQueryRequest request) {
        Map<String, Object> data = buildTradeRequestData(request);
        Map<String, String> result = invoke("trade.query", data);
        TradeQueryResponse response = new TradeQueryResponse();
        initializeTradeResponse(result, response);
        response.setOrderAmount(Integer.parseInt(result.get("order_amount")));
        return response;
    }

    public TradeRateQueryResponse sendTradeRateQueryRequest() {
        Map<String, String> result = invoke("trade.rate.query", new HashMap<String, Object>());
        TradeRateQueryResponse response = new TradeRateQueryResponse();
        response.setInstallmentRate3(Double.parseDouble(result.get("installment_rate_3")));
        response.setInstallmentRate6(Double.parseDouble(result.get("installment_rate_6")));
        response.setInstallmentRate12(Double.parseDouble(result.get("installment_rate_12")));
        response.setInstallmentDiscount(Double.parseDouble(result.get("installment_discount")));
        return response;
    }

    public TradeRefundResponse sendTradeRefundRequest(TradeRefundRequest request) {
        Map<String, Object> data = buildTradeRequestData(request);
        data.put("refund_amount", request.getRefundAmount());
        data.put("refund_reason", request.getRefundReason());
        data.put("merchant_refund_id", request.getMerchantRefundId());

        Map<String, String> result = invoke("trade.refund", data);
        TradeRefundResponse response = new TradeRefundResponse();

        response.setRefundAmount(Integer.parseInt(result.get("refund_amount")));
        response.setRefundTotalAmount(Integer.parseInt(result.get("refund_total_amount")));
        response.setMerchantRefundId(result.get("merchant_refund_id"));
        response.setMerchantOrderId(result.get("merchant_order_id"));
        return response;
    }

    public TradeCancelResponse sendTradeCancelRequest(TradeCancelRequest request) {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("merchant_order_id", request.getMerchantOrderId());

        Map<String, String> result = invoke("trade.cancel", data);
        TradeCancelResponse response = new TradeCancelResponse();
        
        response.setMerchantOrderId(result.get("merchant_order_id"));
        return response;
    }

    public TradeStatementDownloadurlQueryResponse sendTradeStatementDownloadurlQueryRequest(TradeStatementDownloadurlQueryRequest request) {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("statement_date", request.getStatementDate());

        Map<String, String> result = invoke("trade.statement.downloadurl.query", data);
        TradeStatementDownloadurlQueryResponse response = new TradeStatementDownloadurlQueryResponse();
        
        response.setStatementDownloadUrl(result.get("statement_download_url"));
        return response;
    }

    public TradeRefundResponse sendTradeRefundQueryRequest(TradeRefundQueryRequest request) {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("merchant_refund_id", request.getMerchantRefundId());

        Map<String, String> result = invoke("trade.refund.query", data);
        TradeRefundResponse response = new TradeRefundResponse();

        response.setRefundAmount(Integer.parseInt(result.get("refund_amount")));
        response.setRefundTotalAmount(Integer.parseInt(result.get("refund_total_amount")));
        response.setMerchantRefundId(result.get("merchant_refund_id"));
        response.setMerchantOrderId(result.get("merchant_order_id"));
        return response;
    }    

    public PayNotification fetchPayNotification(Map<String, String[]> params) {
        PayNotification result = new PayNotification();
        Map<String, String> data = disassembleData(fetchParams(params));
        result.setMerchantOrderId(data.get("merchant_order_id"));
        result.setTradeNumber(data.get("trade_number"));
        result.setTradeStatus(TradeStatus.forName(data.get("trade_status")));
        result.setOrderAmount(Integer.parseInt(data.get("order_amount")));
        result.setBillUrl(data.get("bill_url"));
        return result;
    }

    public String buildPayRequestHtml(PayRequest request) {
        Map<String, String> entries = buildPayRequestMap(request);
        String sdkUrl = config.getPayMobileUrl();
        /*if (request.getSourceType() != null) {
            if (request.getSourceType().equalsIgnoreCase("android") ||
                    request.getSourceType().equalsIgnoreCase("ios") ||
                    request.getSourceType().equalsIgnoreCase("wap")) {
                sdkUrl = config.getPayMobileUrl();
            }
            if (request.getSourceType().equalsIgnoreCase("pc")) {
                sdkUrl = config.getPayUrl();
            }
        }*/
        StringBuilder sb = new StringBuilder(
            "<!DOCTYPE html><html><head><meta charset=\"utf-8\"/></head><body onload=\"submit()\"><form method=\"post\" action=\""
        ).append(sdkUrl).append("\" id=\"pay-form\">");
        for (Entry<String, String> entry : entries.entrySet()) {
            sb.append("<input type=\"hidden\" name=\"").append(entry.getKey())
                .append("\" value=\"").append(entry.getValue()).append("\">");
        }
        sb.append("</form><script>function submit() {document.getElementById(\"pay-form\").submit();}</script></body></html>");
        return sb.toString();
    }

    public Map<String, String> buildPayRequestMap(PayRequest request) {
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("merchant_order_id", request.getMerchantOrderId());
        data.put("periods", request.getPeriods());
        data.put("tourist_adult_number", request.getTouristAdultNumber());
        data.put("tourist_kid_number", request.getTouristKidNumber());
        data.put("tourist_baby_number", request.getTouristBabyNumber());
        data.put("product_id", request.getProductId());
        data.put("product_name", request.getProductName());
        data.put("product_type", request.getProductType());
        data.put("product_url", request.getProductUrl());
        data.put("order_amount", request.getOrderAmount());
        data.put("time_limit", request.getTimeLimit());
        data.put("flight_number", request.getFlightNumber());
        data.put("departure", request.getDeparture());
        data.put("arrival", request.getArrival());
        data.put("departure_date", request.getDepartureDate());
        data.put("return_date", request.getReturnDate());
        data.put("departure_time", request.getDepartureTime());
        data.put("arrival_time", request.getArrivalTime());
        data.put("hotel_class", request.getHotelClass());
        data.put("source_type", request.getSourceType());
        data.put("return_url", request.getReturnUrl());
        data.put("notify_url", request.getNotifyUrl());
        if (request.getExtraParam() != null) {
            data.put("extra_param", request.getExtraParam());
        }
        ArrayList<Map<String, String>> touristsData = new ArrayList<Map<String, String>>();
        for (PayRequestTourist tourist: request.getTourists()) {
            Map<String, String> touristData = new HashMap<String, String>();
            touristData.put("name", tourist.getName());
            touristData.put("name_spelling", tourist.getNameSpelling());
            touristData.put("id_card_number", tourist.getIdCardNumber());
            touristData.put("mobile", tourist.getMobile());
            touristData.put("email", tourist.getEmail());
            touristsData.add(touristData);
        }
        data.put("tourists", touristsData);
        return assembleData("trade.create", data);
    }

    private Map<String, String> fetchParams(Map<String, String[]> params) {
        Map<String, String> result = new HashMap<String, String>();
        for (Map.Entry<String, String[]> entry : params.entrySet()) {
            String[] values = entry.getValue();
            if (values.length != 1) {
                throw new SfyException("参数错误.");
            }
            result.put(entry.getKey(), values[0]);
        }
        return result;
    }

    private void initializeTradeResponse(
        Map<String, String> result, TradeResponse response
    ) {
        response.setMerchantOrderId(result.get("merchant_order_id"));
        response.setTradeNumber(result.get("trade_number"));
        response.setTradeStatus(TradeStatus.forName(result.get("trade_status")));
    }

    private Map<String, Object> buildTradeRequestData(TradeRequest request) {
        Map<String, Object> data = new HashMap<String, Object>();
        if (request.getMerchantOrderId() != null) {
            data.put("merchant_order_id", request.getMerchantOrderId());
        }
        if (request.getTradeNumber() != null) {
            data.put("trade_number", request.getTradeNumber());
        }
        return data;
    }

    private String convertToJson(Object object) {
        Gson gson = new GsonBuilder().create();
        return gson.toJson(object);
    }

    private Map<String, String> convertJsonToMap(String json) {
        return new Gson().fromJson(json, new TypeToken<HashMap<String, String>>(){}.getType());
    }

    private Map<String, String> invoke(String method, Map<String, Object> data) {
        try {
            URL url = new URL(config.getApiUrl());
            HttpURLConnection connection = (HttpURLConnection)url.openConnection();
            connection.setRequestMethod("POST");
            connection.setRequestProperty(
                "Content-Type", "application/x-www-form-urlencoded"
            );
            connection.setDoOutput(true);
            StringBuilder sb = new StringBuilder();
            Map<String, String>fields = assembleData(method, data);
            for (Map.Entry<String,String> field : fields.entrySet()) {
                if (sb.length() != 0) {
                    sb.append('&');
                }
                sb.append(URLEncoder.encode(field.getKey(), "UTF-8"));
                sb.append('=');
                sb.append(URLEncoder.encode(String.valueOf(field.getValue()), "UTF-8"));
            }
            DataOutputStream os = new DataOutputStream(connection.getOutputStream());
            try {
                os.writeBytes(sb.toString());
                os.flush();
            } finally {
                os.close();
            }
            int responseCode = connection.getResponseCode();
            if (responseCode != 200) {
                throw new SfyException("Api 请求异常.");
            }
            sb = new StringBuilder();
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(connection.getInputStream())
            );
            try {
                String line;
                while ((line = reader.readLine()) != null) {
                    sb.append(line);
                }
            } finally {
                reader.close();
            }
            return disassembleData(
                convertJsonToMap(sb.toString())
            );
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private Map<String, String> assembleData(String method, Map<String, Object> data) {
        HashMap<String, String> result = new HashMap<String, String>();
        Date date = new Date();
        result.put("timestamp", String.valueOf(date.getTime()));
        result.put("merchant_code", config.getMerchantCode());
        result.put("charset", "UTF-8");
        result.put("version", config.getVersion());
        result.put("biz_content", encrypt(convertToJson(data), config.getPrivateKey()));
        result.put("method", method);
        result.put("sign", sign(result));
        return result;
    }

    private Map<String,String> disassembleData(Map<String,String> data) {
        if (checkSign(data) == false) {
            throw new SfyException("签名错误.");
        }
        Map<String,String> result = convertJsonToMap(
            decrypt(data.get("biz_content"), config.getPrivateKey())
        );
        if (result.containsKey("code")) {
            int code = Integer.valueOf(result.get("code"));
            if (code != 10000) {
                throw new SfyException(result.get("message"), code, result.get("merchant_order_id"));
            }
        }
        return result;
    }

    private boolean checkSign(Map<String, String> data) {
        String sign = data.get("sign");
        data.remove("sign");
        return sign(data).equals(sign);
    }

    private String encrypt(String data, String privateKey) {
        try {
            return URLEncoder.encode(
                Base64.encodeBase64String(
                    encryptUsing3Des(data.getBytes("UTF-8"), privateKey)
                ),
                "UTF-8"
            );
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private String decrypt(String data, String privateKey) {
        try {
            return new String(decryptUsing3Des(
                Base64.decodeBase64(
                    URLDecoder.decode(data, "UTF-8")
                ),
                privateKey
            ));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private String sign(Map<String, String> data) {
        SortedSet<String> keys = new TreeSet<String>(data.keySet());
        String s = "";
        for (String key : keys) {
            s += data.get(key);
        }
        s += config.getPrivateKey();
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            md.update(s.getBytes("UTF-8"));
            byte[] digest = md.digest();
            BigInteger tmp = new BigInteger(1, digest);
            String result = tmp.toString(16);
            while (result.length() < 64) {
                result = "0" + result;
            }
            return result;
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    private byte[] encryptUsing3Des(byte[] data, String key) {
        return handle3Des(Cipher.ENCRYPT_MODE, data, key);
    }

    private byte[] decryptUsing3Des(byte[] data, String key) {
        return handle3Des(Cipher.DECRYPT_MODE, data, key);
    }

    private byte[] handle3Des(int mode, byte[] data, String key) {
        try {
            byte[] k = key.getBytes("UTF-8");
            SecretKey sk = new SecretKeySpec(k, "DESede");
            Cipher c = Cipher.getInstance("DESede/ECB/PKCS5Padding");
            c.init(mode, sk);
            return c.doFinal(data);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
